/**
 *
 * The MIT License
 *
 * Copyright (c) 2008 Board of Trustees, Leland Stanford Jr. University. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:

 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.

 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.macdadi.ks.client.views;

import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.*;
import com.gwtext.client.core.EventObject;
import com.gwtext.client.core.RegionPosition;
import com.gwtext.client.widgets.Button;
import com.gwtext.client.widgets.Panel;
import com.gwtext.client.widgets.TabPanel;
import com.gwtext.client.widgets.Window;
import com.gwtext.client.widgets.event.ButtonListenerAdapter;
import com.gwtext.client.widgets.layout.BorderLayout;
import com.gwtext.client.widgets.layout.BorderLayoutData;

import org.macdadi.core.client.auth.AuthService;
import org.macdadi.core.client.auth.AuthenticatedUser;
import org.macdadi.core.client.auth.AuthorizationException;
import org.macdadi.core.client.domain.*;
import org.macdadi.core.client.ui.DefaultItemEditor;
import org.macdadi.core.client.ui.EditableTree;
import org.macdadi.ks.client.View;
import org.macdadi.ks.client.ViewContainer;
import org.macdadi.ks.client.components.CreateAccountPanel;

import java.util.ArrayList;

/**
 * @author Ben Suter <ben.suter@gmail.com>
 */

public class Stakeholders extends View implements TreeListener
{
	protected VerticalPanel mainPanel; 
	protected Grid treeGrid;
	protected EditableTree stakeholdersTree, designersTree, decisionMakersTree, gateKeepersTree;
	protected ArrayList stakeholdersExpandedItems = new ArrayList();
	protected ArrayList designersExpandedItems = new ArrayList();
	protected ArrayList GateKeeperExpandedItems = new ArrayList();
	protected ArrayList decisionMakersExpandedItems = new ArrayList();

	protected ProjectInfo project;

	protected EditableTree.ItemEditor designerAdd, designerRemove;
	protected EditableTree.ItemEditor GateKeeperAdd, GateKeeperRemove;
	protected EditableTree.ItemEditor decisionMakerAdd, decisionMakerRemove;
	protected EditableTree.ItemEditor groupAdd, groupRemove, groupEdit, memberAdd, memberRemove;
	private static String instructionsString;
	private static String bugsString;
	Button instructionsButton, signupButton;

	public static ViewInfo init()
	{
		String info = "<p>View and manage stakeholders, designers, decision makers and project membership</p>";

		return new ViewInfo("Project Team", info)
		{
			public View createInstance(ViewContainer container)
			{
				return new Stakeholders(container);
			}

			public String getColor()
			{
				return "#bf2a2a";
			}
		};
	}

	public Stakeholders(ViewContainer container)
	{
		this.container = container;
		project = container.getCurrentProject();

		mainPanel = new VerticalPanel();
		mainPanel.setStyleName("macdadi-Stakeholders");
		mainPanel.setWidth("98%");

		HorizontalPanel hp = new HorizontalPanel();

		treeGrid = new Grid(1, 4);
		hp.add(treeGrid);

		stakeholdersTree = new EditableTree();
		treeGrid.setWidget(0, 0, stakeholdersTree);
		stakeholdersTree.addTreeListener(this);
		
		gateKeepersTree = new EditableTree();
		treeGrid.setWidget(0, 1, gateKeepersTree);
		gateKeepersTree.addTreeListener(this);

		designersTree = new EditableTree();
		treeGrid.setWidget(0, 2, designersTree);
		designersTree.addTreeListener(this);

		decisionMakersTree = new EditableTree();
		treeGrid.setWidget(0, 3, decisionMakersTree);
		decisionMakersTree.addTreeListener(this);

		groupAdd = new GroupAdd(expandSelectedThenUpdateStakeholdersTree, project);
		groupEdit = new GroupEdit(updateStakeholdersTree, project);
		groupRemove = new GroupRemove(updateStakeholdersTree, project);
		memberAdd = new MemberAdd(expandSelectedThenUpdateStakeholdersTree, project);
		memberRemove = new MemberRemove(updateStakeholdersTree, project);

		designerAdd = new DesignerAdd(expandSelectedThenUpdateDesignersTree, project);
		designerRemove = new DesignerRemove(updateDesignersTree, project);

		GateKeeperAdd = new GateKeeperAdd(expandSelectedThenUpdateGateKeepersTree, project);
		GateKeeperRemove = new GateKeeperRemove(updateGateKeepersTree, project);

		decisionMakerAdd = new DecisionMakerAdd(expandSelectedThenUpdateDecisionMakersTree, project);
		decisionMakerRemove = new DecisionMakerRemove(updateDecisionMakersTree, project);

		Panel space = new Panel();
		space.setHeight(20);
		space.setBorder(false);
		
		Panel space2 = new Panel();
		space2.setHeight(20);
		space2.setBorder(false);

		instructionsString = "<p> On this page, a grid containing four Editable Trees is displayed, and for each tree that the user is authorized to edit the user can: </p>" +
		"<p> ~~~ </p> " +
		"<p><b>   1) Add members from the existing database </b></p>" +
		"<p><b>   2) Remove members from the existing database </b></p>" +
		"<p> ~~~ </p> " +
		"<p> Remarks - This page determines the role each project member has. Depending on which editable tree the project member is added to, this determines the authorization of the project member and based on this the user will and will not be allowed to perform certain actions later on. </p>";
		bugsString = "<p> <b>BUG #1</b>: The authorization system is still under maintenance. There are existing problems where an authorized user cannot perform the correct actions and vice versa.(STATUS: CURRENTLY UNDER MAINTENANCE) </p>" +
		"<p> <b>FIX #1</b>: Upon adding a project member to the project, the new user will automatically be given authorization as a designer, stakeholder, and decision-maker. However, for the purposes of a class one can still add each user to a specific editable tree (i.e. Stakeholders) in order to show compartmentalization of users. </p>" + 
		"<p> ~~~ </p>" +
		"<p> Discover any bugs? Please contact me at: <i>jwei512@stanford.edu</i> </p>";

		HTML instructionsHTML = new HTML(instructionsString);

		TabPanel tabPanel = new TabPanel();  
		tabPanel.setActiveTab(0);

		Panel tab1 = new Panel();  
		tab1.setTitle("Instructions");  
		tab1.add(instructionsHTML);
		tab1.setAutoScroll(true);  

		Panel tab2 = new Panel();  
		tab2.setTitle("Current Bugs/Fixes");  
		tab2.setHtml(bugsString);  
		tab2.setAutoScroll(true);

		tabPanel.add(tab1);
		tabPanel.add(tab2);

		BorderLayoutData centerData = new BorderLayoutData(RegionPosition.CENTER);  
		centerData.setMargins(3, 0, 3, 3);

		final Window window = new Window();
		window.setTitle("Instructions for Project Team Tab");  
		window.setClosable(true);  
		window.setWidth(600);  
		window.setHeight(350);  
		window.setPlain(true); 
		window.setLayout(new BorderLayout());  
		window.add(tabPanel, centerData);
		window.setCloseAction(Window.HIDE);

		instructionsButton = new Button("Instructions");  
		instructionsButton.addListener(new ButtonListenerAdapter() {
			public void onClick(Button button, EventObject e) {
				window.show();
			}  
		});
		
		VerticalPanel signupBox = new VerticalPanel();
		signupBox.add(new CreateAccountPanel());
		
		final Window signupWindow = new Window();
		signupWindow.setTitle("Sign up your friend!");
		signupWindow.setClosable(true);
		signupWindow.setWidth(240);
		signupWindow.setHeight(180);
		signupWindow.setPlain(true);
		signupWindow.setLayout(new BorderLayout());
		signupWindow.add(signupBox, centerData);
		signupWindow.setCloseAction(Window.HIDE);
		
		signupButton = new Button("Invite A Friend");
		signupButton.addListener(new ButtonListenerAdapter() {
			public void onClick(Button button, EventObject e) {
				signupWindow.show();
			}
		});

		mainPanel.add(instructionsButton);
		mainPanel.add(space);
		mainPanel.add(signupButton);
		mainPanel.add(space2);
		mainPanel.add(hp);

		initWidget(mainPanel);
	}

	public void onShow()
	{
		populateStakeholdersTree(project);
		populateDesignersTree(project);
		populateGateKeepersTree(project);
		populateDecisionMakersTree(project);

		AuthenticatedUser user = container.getCurrentUser();

		if (user.isAuthorizedOperation(project.getId(), AuthService.TARGET_STAKEHOLDERS, AuthService.OPERATION_CREATE)) {
			stakeholdersTree.addItemEditor(groupAdd);
			stakeholdersTree.addItemEditor(memberAdd);
			designersTree.addItemEditor(designerAdd);
			gateKeepersTree.addItemEditor(GateKeeperAdd);
			decisionMakersTree.addItemEditor(decisionMakerAdd);
		}

		if (user.isAuthorizedOperation(project.getId(), AuthService.TARGET_STAKEHOLDERS, AuthService.OPERATION_REMOVE)) {
			stakeholdersTree.addItemEditor(groupRemove);
			stakeholdersTree.addItemEditor(memberRemove);
			designersTree.addItemEditor(designerRemove);
			gateKeepersTree.addItemEditor(GateKeeperRemove);
			decisionMakersTree.addItemEditor(decisionMakerRemove);
		}

		if (user.isAuthorizedOperation(project.getId(), AuthService.TARGET_STAKEHOLDERS, AuthService.OPERATION_UPDATE))
			stakeholdersTree.addItemEditor(groupEdit);

	}

	public void onHide()
	{
		stakeholdersTree.stopEditing();
		stakeholdersTree.removeItemEditor(groupAdd);
		stakeholdersTree.removeItemEditor(groupRemove);
		stakeholdersTree.removeItemEditor(groupEdit);
		stakeholdersTree.removeItemEditor(memberAdd);
		stakeholdersTree.removeItemEditor(memberRemove);
		stakeholdersTree.clear();

		designersTree.stopEditing();
		designersTree.removeItemEditor(designerAdd);
		designersTree.removeItemEditor(designerRemove);
		designersTree.clear();

		gateKeepersTree.stopEditing();
		gateKeepersTree.removeItemEditor(GateKeeperAdd);
		gateKeepersTree.removeItemEditor(GateKeeperRemove);
		gateKeepersTree.clear();

		decisionMakersTree.stopEditing();
		decisionMakersTree.removeItemEditor(decisionMakerAdd);
		decisionMakersTree.removeItemEditor(decisionMakerRemove);
		decisionMakersTree.clear();
	}

	/**
	 * Reload the current user object, to refresh the user's
	 * privilege meta-data. This method should be called
	 * whenever a privilege may have been added
	 * or removed from the currently authenticated user.
	 *
	 * The purpose of this method is simply to allow the UI to update
	 * itself for the current user as a convenience: for example,
	 * if the decision-maker makes themself a stakeholder, they should
	 * be able to start adding goals immediately, without first logging
	 * out and then in again.
	 *
	 * We invoke this on: stakeholder add/remove, designer add/remove,
	 * and decision-maker remove.
	 */
	protected void reloadCurrentUser()
	{
		container.getAuthenticationControl().reloadAuthenticatedUser();
	}

	/**
	 * Add a new root item to the tree, whose label initially displays a "loading ..." message.
	 * Then it retrieves all the root-level groups for this project and adds each one to the
	 * tree. As it does so, the children (Users) of these groups are added.
	 * <p/>
	 * Finally the rootItem is expanded, so that the first level of groups
	 * is always visible.
	 *
	 * @param project the project from which the tree domain should be pulled
	 */
	protected void populateStakeholdersTree(final ProjectInfo project)
	{
		final TreeItem rootItem = new TreeItem();
		rootItem.setText("Loading stakeholders for " + project.getName());
		stakeholdersTree.addItem(rootItem);

		DataService.App.getInstance().getStakeholderGroups(project.getId(), container.getCurrentAuthToken(), new AsyncCallback()
		{
			public void onFailure(Throwable caught)
			{
				GWT.log("Failed to populate the stakeholders tree", caught);

				if (caught instanceof AuthorizationException) {
					rootItem.setText("Please login to view stakeholders for " + project.getName());
				}
			}

			public void onSuccess(Object result)
			{
				rootItem.setHTML("<p><font size = 5><b> Stakeholders </b></font></p>");

				StakeholderGroup[] roots = (StakeholderGroup[]) result;
				for (int i = 0; i < roots.length; i++) {
					addStakeholdersChild(rootItem, roots[i]);
				}

				/* Expand the first level */
				rootItem.setState(true, false);
			}
		});
	}

	/**
	 * Adds the given StakeholderGroup as a child TreeItem to the parent TreeItem.
	 * Steps into the given group's members and adds each of those too.
	 *
	 * @param parent the tree node to act as parent for the newly added StakeholderGroup
	 * @param child  the StakeholderGroup to be added as a child node to the tree
	 */
	protected void addStakeholdersChild(TreeItem parent, StakeholderGroup child)
	{
		TreeItem node = new TreeItem();
		node.setHTML("<p><font size = 3><i>" + child.getName() + "</i></font></p>");
		node.setUserObject(child);
		parent.addItem(node);

		for (int i = 0; i < child.getNumMembers(); i++)
			addStakeholdersChild(node, child.getMember(i));

		/* If this node, as identified by its group's id, was "expanded" in a
		 * previous incarnation of this tree, then expand it here again.
		 */
		if (stakeholdersExpandedItems.contains(computeIdFromShg(child)))
			node.setState(true, false);
	}

	/**
	 * Adds the given User as a child TreeItem to the parent TreeItem. It is
	 * expected that the parent TreeItem represents a StakeholderGroup.
	 *
	 * @param parent the StakeholderGroup TreeItem
	 * @param child  the member (User)
	 */
	protected void addStakeholdersChild(TreeItem parent, User child)
	{
		TreeItem node = new TreeItem();
		node.setHTML("<font size = 2>" + child.getName() + "</font>");
		node.setUserObject(child);
		parent.addItem(node);

		/* If this node, as identified by its stakeholder's id, was "expanded" in a
		 * previous incarnation of this tree, then expand it here again.
		 */
		if (stakeholdersExpandedItems.contains(computeIdFromUser(child)))
			node.setState(true, false);
	}

	/**
	 * Add a new root item to the tree, whose label initially displays a "loading ..." message.
	 * Then it retrieves all the root-level groups for this project and adds each one to the
	 * tree. As it does so, the children (Users) of these groups are added.
	 * <p/>
	 * Finally the rootItem is expanded, so that the first level of groups
	 * is always visible.
	 *
	 * @param project the project from which the tree domain should be pulled
	 */
	protected void populateDesignersTree(final ProjectInfo project)
	{
		final TreeItem rootItem = new TreeItem();
		rootItem.setText("Loading designers for " + project.getName());
		designersTree.addItem(rootItem);

		DataService.App.getInstance().getDesigners(project.getId(), container.getCurrentAuthToken(), new AsyncCallback()
		{
			public void onFailure(Throwable caught)
			{
				GWT.log("Failed to populate the designers tree", caught);

				if (caught instanceof AuthorizationException) {
					rootItem.setText("Please login to view designers for " + project.getName());
				}
			}

			public void onSuccess(Object result)
			{
				rootItem.setHTML("<p><font size = 5><b> Designers </b></font></p>");

				User[] roots = (User[]) result;
				for (int i = 0; i < roots.length; i++) {
					addDesignersChild(rootItem, roots[i]);
				}

				/* Expand the first level */
				rootItem.setState(true, false);
			}
		});
	}

	/**
	 * Adds the given User as a child TreeItem to the parent TreeItem. It is
	 * expected that the parent TreeItem represents a StakeholderGroup.
	 *
	 * @param parent the StakeholderGroup TreeItem
	 * @param child  the member (User)
	 */
	protected void addDesignersChild(TreeItem parent, User child)
	{
		TreeItem node = new TreeItem();
		node.setHTML("<font size = 2>" + child.getName() + "</font>");
		node.setUserObject(child);
		parent.addItem(node);

		/* If this node, as identified by its stakeholder's id, was "expanded" in a
		 * previous incarnation of this tree, then expand it here again.
		 */
		if (designersExpandedItems.contains(computeIdFromUser(child)))
			node.setState(true, false);
	}

	/**
	 * Add a new root item to the tree, whose label initially displays a "loading ..." message.
	 * Then it retrieves all the root-level groups for this project and adds each one to the
	 * tree. As it does so, the children (Users) of these groups are added.
	 * <p/>
	 * Finally the rootItem is expanded, so that the first level of groups
	 * is always visible.
	 *
	 * @param project the project from which the tree domain should be pulled
	 */
	protected void populateGateKeepersTree(final ProjectInfo project)
	{
		final TreeItem rootItem = new TreeItem();
		rootItem.setText("Loading Gatekeepers for " + project.getName());
		gateKeepersTree.addItem(rootItem);

		DataService.App.getInstance().getGateKeepers(project.getId(), true, -1, container.getCurrentAuthToken(), new AsyncCallback()
		{
			public void onFailure(Throwable caught)
			{
				GWT.log("Failed to populate the Gatekeepers tree", caught);

				if (caught instanceof AuthorizationException) {
					rootItem.setText("Please login to view project members for " + project.getName());
				}
			}

			public void onSuccess(Object result)
			{
				rootItem.setHTML("<p><font size = 5><b> Gatekeepers </b></font></p>");

				User[] roots = (User[]) result;
				for (int i = 0; i < roots.length; i++) {
					addGateKeeperChild(rootItem, roots[i]);
				}

				/* Expand the first level */
				rootItem.setState(true, false);
			}
		});
	}

	/**
	 * Adds the given User as a child TreeItem to the parent TreeItem. It is
	 * expected that the parent TreeItem represents a StakeholderGroup.
	 *
	 * @param parent the StakeholderGroup TreeItem
	 * @param child  the member (User)
	 */
	protected void addGateKeeperChild(TreeItem parent, User child)
	{
		TreeItem node = new TreeItem();
		node.setHTML("<font size = 2>" + child.getName() + "</font>");
		node.setUserObject(child);
		parent.addItem(node);

		/* If this node, as identified by its stakeholder's id, was "expanded" in a
		 * previous incarnation of this tree, then expand it here again.
		 */
		if (GateKeeperExpandedItems.contains(computeIdFromUser(child)))
			node.setState(true, false);
	}

	/**
	 * Add a new root item to the tree, whose label initially displays a "loading ..." message.
	 * Then it retrieves all the root-level groups for this project and adds each one to the
	 * tree. As it does so, the children (Users) of these groups are added.
	 * <p/>
	 * Finally the rootItem is expanded, so that the first level of groups
	 * is always visible.
	 *
	 * @param project the project from which the tree domain should be pulled
	 */
	protected void populateDecisionMakersTree(final ProjectInfo project)
	{
		final TreeItem rootItem = new TreeItem();
		rootItem.setText("Loading decision makers for " + project.getName());
		decisionMakersTree.addItem(rootItem);

		DataService.App.getInstance().getDecisionMakers(project.getId(), container.getCurrentAuthToken(), new AsyncCallback()
		{
			public void onFailure(Throwable caught)
			{
				GWT.log("Failed to populate the decision makers tree", caught);

				if (caught instanceof AuthorizationException) {
					rootItem.setText("Please login to view decision makers for " + project.getName());
				}
			}

			public void onSuccess(Object result)
			{
				rootItem.setHTML("<p><font size = 5><b> Decision Makers </b></font></p>");

				User[] roots = (User[]) result;
				for (int i = 0; i < roots.length; i++) {
					addDecisionMakersChild(rootItem, roots[i]);
				}

				/* Expand the first level */
				rootItem.setState(true, false);
			}
		});
	}

	/**
	 * Adds the given User as a child TreeItem to the parent TreeItem. It is
	 * expected that the parent TreeItem represents a StakeholderGroup.
	 *
	 * @param parent the StakeholderGroup TreeItem
	 * @param child  the member (User)
	 */
	protected void addDecisionMakersChild(TreeItem parent, User child)
	{
		TreeItem node = new TreeItem();
		node.setHTML("<font size = 2>" + child.getName() + "</font>");
		node.setUserObject(child);
		parent.addItem(node);

		/* If this node, as identified by its stakeholder's id, was "expanded" in a
		 * previous incarnation of this tree, then expand it here again.
		 */
		if (decisionMakersExpandedItems.contains(computeIdFromUser(child)))
			node.setState(true, false);
	}

	public void onTreeItemSelected(TreeItem item)
	{

	}

	/**
	 * Remember the expansion state of this item, so that we can later
	 * return the tree to the same state when it is refreshed.
	 *
	 * @param item the item whose expanded/folded state changed
	 */
	public void onTreeItemStateChanged(TreeItem item)
	{
		/* Remember the expansion state for later tree refresh, based on ids and object types. */
		String id;
		id = computeIdFromUserObject(item.getUserObject());
		if (id == null)
			return;

		if (item.getTree().equals(stakeholdersTree)) {
			if (item.getState())
				stakeholdersExpandedItems.add(id);
			else
				stakeholdersExpandedItems.remove(id);
		} else if (item.getTree().equals(designersTree)) {
			if (item.getState())
				designersExpandedItems.add(id);
			else
				designersExpandedItems.remove(id);
		} else if (item.getTree().equals(gateKeepersTree)) {
			if (item.getState())
				GateKeeperExpandedItems.add(id);
			else
				GateKeeperExpandedItems.remove(id);
		} else if (item.getTree().equals(decisionMakersTree)) {
			if (item.getState())
				decisionMakersExpandedItems.add(id);
			else
				decisionMakersExpandedItems.remove(id);
		}
	}

	// TODO: practically we can ignore users, because they (for now) don't
	// contain children so are never expanded !
	protected String computeIdFromUserObject(Object userObject)
	{
		if (userObject instanceof User)
			return computeIdFromUser((User) userObject);
		else if (userObject instanceof StakeholderGroup)
			return computeIdFromShg((StakeholderGroup) userObject);
		else
			return null;
	}

	protected String computeIdFromUser(User u)
	{
		return "user-" + Integer.toString(u.getId());
	}

	protected String computeIdFromShg(StakeholderGroup shg)
	{
		return "shg-" + Integer.toString(shg.getId());
	}


	/**
	 * A command that refreshes the tree.
	 */
	protected Command updateStakeholdersTree = new Command()
	{
		public void execute()
		{
			stakeholdersTree.clear();
			populateStakeholdersTree(project);
		}
	};

	/**
	 * A command that refreshes the tree.
	 */
	protected Command updateDesignersTree = new Command()
	{
		public void execute()
		{
			designersTree.clear();
			populateDesignersTree(project);
		}
	};

	/**
	 * A command that refreshes the tree.
	 */
	protected Command updateGateKeepersTree = new Command()
	{
		public void execute()
		{
			gateKeepersTree.clear();
			populateGateKeepersTree(project);
		}
	};

	/**
	 * A command that refreshes the tree.
	 */
	protected Command updateDecisionMakersTree = new Command()
	{
		public void execute()
		{
			decisionMakersTree.clear();
			populateDecisionMakersTree(project);
		}
	};

	/**
	 * A command that first expands the selected tree item, and then
	 * refreshes the tree. This is typically used when a new child
	 * gets added to a node, so that the new child will be visible
	 * after the refresh.
	 */
	protected Command expandSelectedThenUpdateStakeholdersTree = new Command()
	{
		public void execute()
		{
			/* If the item being edited is not already expanded, "remember"
			 * it as expanded for after the refresh.
			 */
			TreeItem edited = stakeholdersTree.getSelectedItem();
			if (edited != null) {
				String id;
				id = computeIdFromUserObject(edited.getUserObject());
				if (id != null && !stakeholdersExpandedItems.contains(id))
					stakeholdersExpandedItems.add(id);
			}

			updateStakeholdersTree.execute();
		}
	};

	/**
	 * A command that first expands the selected tree item, and then
	 * refreshes the tree. This is typically used when a new child
	 * gets added to a node, so that the new child will be visible
	 * after the refresh.
	 */
	protected Command expandSelectedThenUpdateDesignersTree = new Command()
	{
		public void execute()
		{
			/* If the item being edited is not already expanded, "remember"
			 * it as expanded for after the refresh.
			 */
			TreeItem edited = designersTree.getSelectedItem();
			if (edited != null) {
				String id;
				id = computeIdFromUserObject(edited.getUserObject());
				if (id != null && !designersExpandedItems.contains(id))
					designersExpandedItems.add(id);
			}
			updateDesignersTree.execute();
		}
	};

	/**
	 * A command that first expands the selected tree item, and then
	 * refreshes the tree. This is typically used when a new child
	 * gets added to a node, so that the new child will be visible
	 * after the refresh.
	 */
	protected Command expandSelectedThenUpdateGateKeepersTree = new Command()
	{
		public void execute()
		{
			/* If the item being edited is not already expanded, "remember"
			 * it as expanded for after the refresh.
			 */
			TreeItem edited = gateKeepersTree.getSelectedItem();
			if (edited != null) {
				String id;
				id = computeIdFromUserObject(edited.getUserObject());
				if (id != null && !GateKeeperExpandedItems.contains(id))
					GateKeeperExpandedItems.add(id);
			}

			updateGateKeepersTree.execute();
		}
	};

	/**
	 * A command that first expands the selected tree item, and then
	 * refreshes the tree. This is typically used when a new child
	 * gets added to a node, so that the new child will be visible
	 * after the refresh.
	 */
	protected Command expandSelectedThenUpdateDecisionMakersTree = new Command()
	{
		public void execute()
		{
			/* If the item being edited is not already expanded, "remember"
			 * it as expanded for after the refresh.
			 */
			TreeItem edited = decisionMakersTree.getSelectedItem();
			if (edited != null) {
				String id;
				id = computeIdFromUserObject(edited.getUserObject());
				if (id != null && !decisionMakersExpandedItems.contains(id))
					decisionMakersExpandedItems.add(id);
			}
			updateDecisionMakersTree.execute();
		}
	};

	/*
	 * Editing:
	 *
	 * root:
	 *   Add StakeholderGroup
	 *
	 * StakeholderGroup:
	 *   Edit name - need groupId
	 *   Remove group (and members from group) - need groupId
	 *   Add member (only if contains no sub-groups) - need groupId, memberId
	 *    choose from project members
	 *    add new project member, from list of users
	 *    create user and add to project
	 *   (FUTURE) Add sub-group (only if contains no members) - need groupId, newName
	 *   (FUTURE) Assign color
	 *
	 * Member:
	 *   Remove from group - need groupId and userId
	 *
	 */

	/**
	 * Lets you add a new group at the root of the tree,
	 * providing an input field for the name of the new group.
	 * <p/>
	 * This editor only matches the root TreeItem.
	 * <p/>
	 * In the future, when nested groups are possible, this editor
	 * will match to group nodes as well as the root.
	 */
	public class GroupAdd extends DefaultItemEditor
	{
		protected ProjectInfo project;

		public GroupAdd(Command onSuccess, ProjectInfo p)
		{
			super(DefaultItemEditor.iconPathAdd, "Add Group", onSuccess);
			project = p;
		}

		public boolean matchesItem(TreeItem item)
		{
			return item.getParentItem() == null;
		}

		protected Widget getInputWidget(Object userObject)
		{
			// note: as long as this only matches to the root, the userObject will always be null
			return configureWidget((StakeholderGroup) userObject);
		}

		protected Widget configureWidget(final StakeholderGroup group)
		{
			/* A panel for editing the name: text input, apply, cancel */
			VerticalPanel vpA = new VerticalPanel();
			final TextBox tbAdd = new TextBox();
			tbAdd.setVisibleLength(50);
			vpA.add(tbAdd);
			HorizontalPanel hpA = new HorizontalPanel();
			Button btnAddDo = new Button("Create");
			btnAddDo.addListener(new ButtonListenerAdapter() {
				public void onClick(Button button, EventObject e) {
					addChildGroup(group, tbAdd.getText().trim());
				}
			});
			hpA.add(btnAddDo);

			Button btnAddCancel = new Button("Reset");
			btnAddCancel.addListener(new ButtonListenerAdapter() {
				public void onClick(Button button, EventObject e) {
					tbAdd.setText("");
				}
			});
			hpA.add(btnAddCancel);
			vpA.add(hpA);

			return vpA;
		}

		protected void addChildGroup(final StakeholderGroup group, String newName)
		{
			if (newName.length() == 0) {
				displayError("You must enter a name for the new group", null);
				return;
			}

			int parent = -1;
			if (group != null)
				parent = group.getId();

			DataWriteService.App.getInstance().createStakeholderGroup(project.getId(), parent, newName, container.getCurrentAuthToken(), this);
		}
	}

	/**
	 * Only matches to TreeItems whose userObject is a StakeholderGroup.
	 */
	public abstract class StakeholderGroupEditor extends DefaultItemEditor
	{
		protected ProjectInfo project;

		public StakeholderGroupEditor(String iconPath, String title, Command onSuccess, ProjectInfo p)
		{
			super(iconPath, title, onSuccess);
			this.project = p;
		}

		/* Only display this for StakeholderGroup nodes */
		public boolean matchesItem(TreeItem item)
		{
			return (item.getUserObject() instanceof StakeholderGroup);
		}

		protected Widget getInputWidget(Object userObject)
		{
			if (userObject == null || !(userObject instanceof StakeholderGroup))
				throw new IllegalArgumentException("userObject must be a valid StakeholderGroup");

			return configureWidget((StakeholderGroup) userObject);
		}

		protected abstract Widget configureWidget(final StakeholderGroup group);
	}

	/**
	 * Lets you edit the StakeholderGroup's name.
	 */
	public class GroupEdit extends StakeholderGroupEditor
	{
		public GroupEdit(Command onSuccess, ProjectInfo p)
		{
			super(DefaultItemEditor.iconPathEdit, "Edit", onSuccess, p);
		}

		protected Widget configureWidget(final StakeholderGroup group)
		{
			/* A panel for editing the name: text input, apply, cancel */
			VerticalPanel vpE = new VerticalPanel();
			final TextBox tbEdit = new TextBox();
			tbEdit.setVisibleLength(50);
			tbEdit.setText(group.getName());
			vpE.add(tbEdit);

			HorizontalPanel hpE = new HorizontalPanel();

			Button btnEditDo = new Button("Apply");
			btnEditDo.addListener(new ButtonListenerAdapter()
			{
				public void onClick(Button button, EventObject e) {
					String newName = tbEdit.getText().trim();
					updateGroup(group, newName);
				}
			});
			hpE.add(btnEditDo);

			Button btnEditCancel = new Button("Reset");
			btnEditCancel.addListener(new ButtonListenerAdapter()
			{
				public void onClick(Button button, EventObject e) {
					tbEdit.setText(group.getName());
				}
			});
			hpE.add(btnEditCancel);
			vpE.add(hpE);

			return vpE;
		}

		protected void updateGroup(final StakeholderGroup group, String newName)
		{
			//considers p and P as the same
			//if (newName.equals(group.getName()))
				//return; /* No change was made */
			DataWriteService.App.getInstance().updateStakeholderGroup(project.getId(), group.getId(), newName, container.getCurrentAuthToken(), this);
		}
	}

	/**
	 * Displays a clickable link to remove the group.
	 */
	public class GroupRemove extends StakeholderGroupEditor
	{
		public GroupRemove(Command onSuccess, ProjectInfo p)
		{
			super(DefaultItemEditor.iconPathRemove, "Remove", onSuccess, p);
		}

		protected Widget configureWidget(final StakeholderGroup group)
		{
			HTML rem = new HTML("<u style='cursor:hand'>Remove this group</u>");
			rem.addClickListener(new ClickListener()
			{
				public void onClick(Widget sender)
				{
					removeGroup(group);
				}
			});

			return rem;
		}

		protected void removeGroup(StakeholderGroup group)
		{
			DataWriteService.App.getInstance().removeStakeholderGroup(project.getId(), group.getId(), container.getCurrentAuthToken(), this);
		}
	}

	/**
	 * Lets you add a member to the StakeholderGroup.
	 */
	public class MemberAdd extends StakeholderGroupEditor
	{
		public MemberAdd(Command onSuccess, ProjectInfo p)
		{
			super(DefaultItemEditor.iconPathAdd, "Add a member", onSuccess, p);
		}

		protected Widget configureWidget(final StakeholderGroup group)
		{
			/* A panel for adding a new member */
			VerticalPanel vp = new VerticalPanel();

			final ListBox lb = new ListBox();
			lb.addItem("Choose a project member", "-1");
			fillAvailableProjectMembers(lb, group);
			vp.add(lb);

			Button btnAdd = new Button("Add to " + group.getName());
			btnAdd.addListener(new ButtonListenerAdapter()
			{
				public void onClick(Button button, EventObject e) {
					int newMember = -1;

					if (lb.getSelectedIndex() > 0) {
						try {
							newMember = Integer.parseInt(lb.getValue(lb.getSelectedIndex()));
						} catch (Exception ex) {
							newMember = -1;
						}
					}

					if (newMember > 0) {
						addMember(group, newMember);
					}
				}
			});
			vp.add(btnAdd);

			return vp;
		}

		protected void addMember(final StakeholderGroup group, int userId)
		{
			DataWriteService.App.getInstance().addMemberToStakeholderGroup(project.getId(), group.getId(), userId, container.getCurrentAuthToken(), this);

			/* If the current user is modifying their own privileges, reload the user object to reflect this. */
			if ( userId == container.getCurrentUser().getId() )
				reloadCurrentUser();
		}

		protected void fillAvailableProjectMembers(final ListBox choices, StakeholderGroup thisGroup)
		{
			DataService.App.getInstance().findUsers(project.getId(), true, false, null, container.getCurrentAuthToken(), new AsyncCallback()
			{
				public void onFailure(Throwable caught)
				{
					displayError("Unable to retrieve the available project members: " + caught.getMessage(), caught);
					if (caught instanceof AuthorizationException) {
						displayError("Please login to view available project members", caught);
					}
				}

				/**
				 * If the result is null this is interpreted as failure.
				 *
				 * @param result the result of the async call, should be an array of users
				 */
				public void onSuccess(Object result)
				{
					if (result == null || !(result instanceof User[])) {
						displayError("Unable to retrieve the available project members", null);
						return;
					}

					User[] members = (User[]) result;
					for (int i = 0; i < members.length; i++) {
						choices.addItem(members[i].getName(), Integer.toString(members[i].getId()));
					}
				}
			});
		}
	}

	/**
	 * Only matches to TreeItems whose userObject is a User.
	 */
	public class MemberRemove extends DefaultItemEditor
	{
		protected ProjectInfo project;

		public MemberRemove(Command onSuccess, ProjectInfo p)
		{
			super(DefaultItemEditor.iconPathRemove, "Remove from group", onSuccess);
			this.project = p;
		}

		/* Only display this for User (member) nodes */
		public boolean matchesItem(TreeItem item)
		{
			/* In the future, may need to check that parent is a group too */
			return (item.getUserObject() instanceof User);
		}

		protected Widget getInputWidget(Object userObject)
		{
			if (userObject == null || !(userObject instanceof User))
				throw new IllegalArgumentException("userObject must be a valid User");

			return configureWidget((User) userObject);
		}


		protected Widget configureWidget(final User member)
		{
			// TODO: add checks to make this safe, or at least make matchesItem tighter
			TreeItem parent = item.getParentItem();
			final StakeholderGroup group = (StakeholderGroup) parent.getUserObject();

			HTML rem = new HTML("<u style='cursor:hand'>Remove this member from the group</u>");
			rem.addClickListener(new ClickListener()
			{
				public void onClick(Widget sender)
				{
					removeMember(group, member);
				}
			});

			return rem;
		}

		protected void removeMember(StakeholderGroup group, User member)
		{
			DataWriteService.App.getInstance().removeMemberFromStakeholderGroup(project.getId(), group.getId(), member.getId(), container.getCurrentAuthToken(), this);

			/* If the current user is modifying their own privileges, reload the user object to reflect this. */
			if ( member.getId() == container.getCurrentUser().getId() )
				reloadCurrentUser();
		}

	}

	/**
	 * Only matches to TreeItems whose userObject is a User.
	 */
	public class DesignerRemove extends DefaultItemEditor
	{
		protected ProjectInfo project;

		public DesignerRemove(Command onSuccess, ProjectInfo p)
		{
			super(DefaultItemEditor.iconPathRemove, "Remove designer", onSuccess);
			this.project = p;
		}

		/* Only display this for User (member) nodes */
		public boolean matchesItem(TreeItem item)
		{
			return (item.getUserObject() instanceof User);
		}

		protected Widget getInputWidget(Object userObject)
		{
			if (userObject == null || !(userObject instanceof User))
				throw new IllegalArgumentException("userObject must be a valid User");

			return configureWidget((User) userObject);
		}

		protected Widget configureWidget(final User user)
		{
			// TODO: add checks to make this safe, or at least make matchesItem tighter

			HTML rem = new HTML("<u style='cursor:hand'>Remove this designer</u>");
			rem.addClickListener(new ClickListener()
			{
				public void onClick(Widget sender)
				{
					removeDesigner(user);
				}
			});

			return rem;
		}

		protected void removeDesigner(User user)
		{
			DataWriteService.App.getInstance().removeDesigner(project.getId(), user.getId(), container.getCurrentAuthToken(), this);

			/* If the current user is modifying their own privileges, reload the user object to reflect this. */
			if ( user.getId() == container.getCurrentUser().getId() )
				reloadCurrentUser();
		}
	}

	/**
	 * Only matches to TreeItems whose userObject is a User.
	 */
	public class GateKeeperRemove extends DefaultItemEditor
	{
		protected ProjectInfo project;

		public GateKeeperRemove(Command onSuccess, ProjectInfo p)
		{
			super(DefaultItemEditor.iconPathRemove, "Remove gatekeeper", onSuccess);
			this.project = p;
		}

		/* Only display this for User (member) nodes */
		public boolean matchesItem(TreeItem item)
		{
			return (item.getUserObject() instanceof User);
		}

		protected Widget getInputWidget(Object userObject)
		{
			if (userObject == null || !(userObject instanceof User))
				throw new IllegalArgumentException("userObject must be a valid User");

			return configureWidget((User) userObject);
		}

		protected Widget configureWidget(final User user)
		{
			// TODO: add checks to make this safe, or at least make matchesItem tighter

			HTML rem = new HTML("<u style='cursor:hand'>Remove this gatekeeper</u>");
			rem.addClickListener(new ClickListener()
			{
				public void onClick(Widget sender)
				{
					removeGateKeeper(user);
				}
			});

			return rem;
		}

		protected void removeGateKeeper(User user)
		{
			DataWriteService.App.getInstance().removeGateKeeper(project.getId(), user.getId(), container.getCurrentAuthToken(), this);
		}
	}

	/**
	 * Only matches to TreeItems whose userObject is a User.
	 */
	public class DecisionMakerRemove extends DefaultItemEditor
	{
		protected ProjectInfo project;

		public DecisionMakerRemove(Command onSuccess, ProjectInfo p)
		{
			super(DefaultItemEditor.iconPathRemove, "Remove decision maker", onSuccess);
			this.project = p;
		}

		/* Only display this for User (member) nodes */
		public boolean matchesItem(TreeItem item)
		{
			return (item.getUserObject() instanceof User);
		}

		protected Widget getInputWidget(Object userObject)
		{
			if (userObject == null || !(userObject instanceof User))
				throw new IllegalArgumentException("userObject must be a valid User");

			return configureWidget((User) userObject);
		}

		protected Widget configureWidget(final User user)
		{
			// TODO: add checks to make this safe, or at least make matchesItem tighter

			HTML rem = new HTML("<u style='cursor:hand'>Remove this decision maker</u>");
			rem.addClickListener(new ClickListener()
			{
				public void onClick(Widget sender)
				{
					removeDecisionMaker(user);
				}
			});

			return rem;
		}

		protected void removeDecisionMaker(User user)
		{
			DataWriteService.App.getInstance().removeDecisionMaker(project.getId(), user.getId(), container.getCurrentAuthToken(), this);

			/* If the current user is modifying their own privileges, reload the user object to reflect this. */
			if ( user.getId() == container.getCurrentUser().getId() )
				reloadCurrentUser();  
		}
	}

	/**
	 * Lets you add a new designer at the root of the tree,
	 * providing a list of current project members.
	 * <p/>
	 * This editor only matches the root TreeItem.
	 */
	public class DesignerAdd extends DefaultItemEditor
	{
		protected ProjectInfo project;

		public DesignerAdd(Command onSuccess, ProjectInfo p)
		{
			super(DefaultItemEditor.iconPathAdd, "Add a designer", onSuccess);
			project = p;
		}

		public boolean matchesItem(TreeItem item)
		{
			return item.getParentItem() == null;
		}

		protected Widget getInputWidget(Object userObject)
		{
			// note: as long as this only matches to the root, the userObject will always be null
			return configureWidget();
		}

		protected Widget configureWidget()
		{
			/* A panel for adding a new member */
			VerticalPanel vp = new VerticalPanel();

			final ListBox lb = new ListBox();
			lb.addItem("Choose a project member", "-1");
			fillAvailableProjectMembers(lb);
			vp.add(lb);

			Button btnAdd = new Button("Add");
			btnAdd.addListener(new ButtonListenerAdapter()
			{
				public void onClick(Button button, EventObject e) {
					int newMember = -1;

					if (lb.getSelectedIndex() > 0) {
						try {
							newMember = Integer.parseInt(lb.getValue(lb.getSelectedIndex()));
						} catch (Exception ex) {
							newMember = -1;
						}
					}

					if (newMember > 0) {
						addDesigner(newMember);
					}
				}
			});
			vp.add(btnAdd);

			return vp;
		}

		protected void addDesigner(int userId)
		{
			DataWriteService.App.getInstance().addDesigner(project.getId(), userId, container.getCurrentAuthToken(), this);

			/* If the current user is modifying their own privileges, reload the user object to reflect this. */
			if ( userId == container.getCurrentUser().getId() )
				reloadCurrentUser();
		}

		protected void fillAvailableProjectMembers(final ListBox choices)
		{
			DataService.App.getInstance().findUsers(project.getId(), true, false, null, container.getCurrentAuthToken(), new AsyncCallback()
			{
				public void onFailure(Throwable caught)
				{
					displayError("Unable to retrieve the available project members: " + caught.getMessage(), caught);
					if (caught instanceof AuthorizationException) {
						displayError("Please login to view available project members", caught);
					}
				}

				/**
				 * If the result is null this is interpreted as failure.
				 *
				 * @param result the result of the async call, should be an array of users
				 */
				public void onSuccess(Object result)
				{
					if (result == null || !(result instanceof User[])) {
						displayError("Unable to retrieve the available project members", null);
						return;
					}

					User[] members = (User[]) result;
					for (int i = 0; i < members.length; i++) {
						choices.addItem(members[i].getName(), Integer.toString(members[i].getId()));
					}
				}
			});
		}
	}

	/**
	 * Lets you add a new decision maker at the root of the tree,
	 * providing a list of current project members.
	 * <p/>
	 * This editor only matches the root TreeItem.
	 */
	public class DecisionMakerAdd extends DefaultItemEditor
	{
		protected ProjectInfo project;

		public DecisionMakerAdd(Command onSuccess, ProjectInfo p)
		{
			super(DefaultItemEditor.iconPathAdd, "Add a decision maker", onSuccess);
			project = p;
		}

		public boolean matchesItem(TreeItem item)
		{
			return item.getParentItem() == null;
		}

		protected Widget getInputWidget(Object userObject)
		{
			// note: as long as this only matches to the root, the userObject will always be null
			return configureWidget();
		}

		protected Widget configureWidget()
		{
			/* A panel for adding a new member */
			VerticalPanel vp = new VerticalPanel();

			final ListBox lb = new ListBox();
			lb.addItem("Choose a project member", "-1");
			fillAvailableProjectMembers(lb);
			vp.add(lb);

			Button btnAdd = new Button("Add");
			btnAdd.addListener(new ButtonListenerAdapter()
			{
				public void onClick(Button button, EventObject e) {
					int newMember = -1;

					if (lb.getSelectedIndex() > 0) {
						try {
							newMember = Integer.parseInt(lb.getValue(lb.getSelectedIndex()));
						} catch (Exception ex) {
							newMember = -1;
						}
					}

					if (newMember > 0) {
						addDecisionMaker(newMember);
					}
				}
			});
			vp.add(btnAdd);

			return vp;
		}

		protected void addDecisionMaker(int userId)
		{
			DataWriteService.App.getInstance().addDecisionMaker(project.getId(), userId, container.getCurrentAuthToken(), this);
		}

		protected void fillAvailableProjectMembers(final ListBox choices)
		{
			DataService.App.getInstance().findUsers(project.getId(), true, false, null, container.getCurrentAuthToken(), new AsyncCallback()
			{
				public void onFailure(Throwable caught)
				{
					displayError("Unable to retrieve the available users: " + caught.getMessage(), caught);
					if (caught instanceof AuthorizationException) {
						displayError("Please login to view available users", caught);
					}
				}

				/**
				 * If the result is null this is interpreted as failure.
				 *
				 * @param result the result of the async call, should be an array of users
				 */
				public void onSuccess(Object result)
				{
					if (result == null || !(result instanceof User[])) {
						displayError("Unable to retrieve the available users", null);
						return;
					}

					User[] members = (User[]) result;
					for (int i = 0; i < members.length; i++) {
						choices.addItem(members[i].getName(), Integer.toString(members[i].getId()));
					}
				}
			});
		}
	}

	/**
	 * Lets you add a new designer at the root of the tree,
	 * providing a list of current project members.
	 * <p/>
	 * This editor only matches the root TreeItem.
	 */
	public class GateKeeperAdd extends DefaultItemEditor
	{
		protected ProjectInfo project;

		public GateKeeperAdd(Command onSuccess, ProjectInfo p)
		{
			super(DefaultItemEditor.iconPathAdd, "Add a gatekeeper", onSuccess);
			project = p;
		}

		public boolean matchesItem(TreeItem item)
		{
			return item.getParentItem() == null;
		}

		protected Widget getInputWidget(Object userObject)
		{
			// note: as long as this only matches to the root, the userObject will always be null
			return configureWidget();
		}

		protected Widget configureWidget()
		{
			/* A panel for adding a new member */
			VerticalPanel vp = new VerticalPanel();

			final ListBox lb = new ListBox();
			lb.addItem("Choose a user", "-1");
			fillAvailableUsers(lb);
			vp.add(lb);

			Button btnAdd = new Button("Add");
			btnAdd.addListener(new ButtonListenerAdapter()
			{
				public void onClick(Button button, EventObject e) {
					int newMember = -1;

					if (lb.getSelectedIndex() > 0) {
						try {
							newMember = Integer.parseInt(lb.getValue(lb.getSelectedIndex()));
						} catch (Exception ex) {
							newMember = -1;
						}
					}

					if (newMember > 0) {
						addGateKeeper(newMember);
					}
				}
			});
			vp.add(btnAdd);

			return vp;
		}

		protected void addGateKeeper(int userId)
		{
			DataWriteService.App.getInstance().addGateKeeper(project.getId(), userId, container.getCurrentAuthToken(), this);
		}

		protected void fillAvailableUsers(final ListBox choices)
		{
			DataService.App.getInstance().findUsers(project.getId(), true, false, null, container.getCurrentAuthToken(), new AsyncCallback()
			{
				public void onFailure(Throwable caught)
				{
					displayError("Unable to retrieve the available users: " + caught.getMessage(), caught);
					if (caught instanceof AuthorizationException) {
						displayError("Please login to view available users", caught);
					}
				}

				/**
				 * If the result is null this is interpreted as failure.
				 *
				 * @param result the result of the async call, should be an array of users
				 */
				public void onSuccess(Object result)
				{
					if (result == null || !(result instanceof User[])) {
						displayError("Unable to retrieve the available users", null);
						return;
					}

					User[] members = (User[]) result;
					for (int i = 0; i < members.length; i++ ) {
						choices.addItem(members[i].getName(), Integer.toString(members[i].getId()));
					}
				}
			});
		}
	}
}
